﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Simulator
{
    public class SimOptimizer
    {

        const int M = 5;//число лучших решений для следующей генерации
        const int E = 4;//число решений, которые генерируются из лучших
        const double eps = 0.01;//
        /// <summary>
        /// Делегат. Уведомляет о ходе оптимизации
        /// </summary>
        /// <param name="b">ИСТИНА, если оптимизация м.б. остановлена</param>
        public delegate void NotifyOpt(bool b);
        /// <summary>
        /// Делегат функции, вызываемой на каждом шаге оптимизационного алгоритма
        /// </summary>
        public NotifyOpt OnNotify;

        private SimModel sim;
        /// <summary>
        /// Получить лучшее на текущем шаге решение задачи оптимизации
        /// </summary>
        public SimModel BestSim { get { return sim; } }


        /// <summary>
        /// Число повторений прогона для каждой конфигурации модели
        /// </summary>
        public int NRepeat { get; set; }

        /// <summary>
        /// Начальная модель, из которой идет оптимизация
        /// </summary>
        public SimModel StartModel { get; set; }


        private bool Paused = false;
        /// <summary>
        /// Список названий категорий ресурсов из БД, хранящий списки ресурсов
        /// </summary>
        public SortedList<string, List<AtomicRes>> Categories { get; set; }

        private SortedList<string, List<AtomicRes>> sortedListAtomic = new SortedList<string, List<AtomicRes>>();
        /// <summary>
        /// ограничения на параметры: максимум, если вес положительный, минимум, если вес отрицательный. Среднее время [0]
        /// </summary>
        public List<double> ParamConstraints { get; set; }
        /// <summary>
        /// веса параметров. Нормированные. Могут быть больше, равны или меньше нуля. Среднее время [0]
        /// </summary>
        public List<double> ParamWeights { get; set; }

        /// <summary>
        /// Изменять множественность параметров
        /// </summary>
        public bool ChangeMultiplicity { get; set; }

        /// <summary>
        /// Заменять атомарные ресурсы на атомарные ресурсы той же категории
        /// </summary>
        public bool ChangeAtomicRes{ get; set; }

        /// <summary>
        /// Получить обобщенный критерий модели при настроенных весах ParamWeight.
        /// </summary>
        /// <param name="sim">Модель</param>
        /// <returns>Обобщенный критерий</returns>
        public double GetGeneralizedCriterion (SimModel sim)
        {
            double criterion = 0;

            if (!double.IsNaN(sim.Tavr))//если среднее время не равно NaN (так м.б. при первом запуске)
            {
                criterion += ParamWeights[0] * sim.Tavr;
            }
            for(int i = 0; i < sim.NParams; i++)
            {
                criterion += ParamWeights[1+i] * sim.GetTotalParam(i);//нулевой параметр веса - это к времени вып.
            }
            return criterion;
        }
        /// <summary>
        /// Рассчитать среднее время обр. проекта
        /// </summary>
        /// <param name="sim">Модель</param>

        private void CalcTavr(ref SimModel sim)
        {
            double buf = 0;
            for (int i = 0; i < NRepeat; i++)
            {
                buf += sim.RunOnce();
            }
            sim.Tavr = buf / (double)NRepeat;
        }

        /// <summary>
        /// Должна ли идти оптимизация
        /// </summary>
        private bool shouldOptimizationGo = false;
        /// <summary>
        /// Пул особей
        /// </summary>
        private List<SimModel> simPool = new List<SimModel>();

        /// <summary>
        /// Запуcтить оптимизацию
        /// </summary>
        public void Run()
        {
            shouldOptimizationGo = true;
            if (!Paused)
            {
                //инициализируем начальную популяцию
                InitiatePool(StartModel);
            }
            SimModel sim;

            double PrevBestCrit, BestCrit = 0;
            double Delta, MaxDelta = Double.MinValue;

            int ctr = 0;
            bool canStop = false;

            while (shouldOptimizationGo)//бесконечный цикл, выкл-й пользователем
            {
                List<SimModel> pool2 = new List<SimModel>();
                //получим E потомков каждого индивида
                foreach (SimModel s in simPool)
                {
                    for (int i = 0; i < E; i++)
                    {
                        sim = s.Clone();
                        Mutate(ref sim);
                        pool2.Add(sim);
                    }
                }
                simPool.AddRange(pool2);//объединим пулы
                //произведем рулеточный отбор всех полученных особей
                RouletteSelection();

                //проверим КОП
                PrevBestCrit = BestCrit;
                int bestId;
                BestCrit = FindBestCriterionValue(out bestId);
                if(ctr > 0)//на всех шагах, кроме нулевого
                {
                    Delta = Math.Abs(BestCrit - PrevBestCrit);
                    if (MaxDelta < Delta)
                        MaxDelta = Delta;
                    //если разница текущего delta и наибольшего (где-то в начале поиска) будет в eps раз.
                    if (Delta < eps * MaxDelta)
                        canStop = true;
                }
                ctr++;
                //сделаем так, чтобы лучшего индивида можно было найти
                this.sim = simPool[bestId];
                //известим о конце цикла
                OnNotify.Invoke(canStop);
            }
        }

        private double FindBestCriterionValue(out int id)
        {
            //найдем лучшего
            id = 0;
            double MinCrit = Double.MaxValue;
            double buf;
            for (int i = 0; i < simPool.Count; i++)
            {
                buf = GetGeneralizedCriterion(simPool[i]);
                if (buf < MinCrit)
                {
                    MinCrit = buf;
                    id = i;
                }
            }
            return MinCrit;
        }

        /// <summary>
        /// Отобрать M решений из M*E имеющихся
        /// </summary>
        private void RouletteSelection()
        {
            List<SimModel> newPool = new List<SimModel>();
            //найдем худшего и лучшего
            double MaxCrit = Double.MinValue;
            double MinCrit = Double.MinValue;
            int minID = 0;
            double[] Crits = new double[simPool.Count];
            for (int i = 0; i < simPool.Count; i++)
            {
                Crits[i] = GetGeneralizedCriterion(simPool[i]);
                if (Crits[i] > MaxCrit)
                    MaxCrit = Crits[i];
                if (Crits[i] < MinCrit)
                {
                    MinCrit = Crits[i];
                    minID = i;
                }
            }
            //настроим инвертные критерии Z[i]
            double[] Z = new double[simPool.Count];
            double SumZ = 0;//сумма всех Z
            for (int i = 0; i < simPool.Count; i++)
            {
                Z[i] = MaxCrit - Crits[i];
                SumZ += Z[i];
            }
            Random r = new Random(System.DateTime.Now.Millisecond);
            int j;
            for (int i = 0; i < M-1; i++)
            {
                //теперь найдем случайное число и выберем лучших
                double rnd = r.NextDouble() * SumZ;
                j = 0;
                while (rnd > Z[j])
                {
                    rnd -= Z[j];
                    j++;
                }
                //Z[j - 1] - выбранный элемент
                if (j - 1 < 0)
                    j = 1;
                newPool.Add(simPool[j - 1]);//добавим его в новый пул
            }
            newPool.Add(simPool[minID]);//лучшего в любом случае добавим, хоть несколько раз
            //заменим пулы
            simPool.Clear();
            simPool.AddRange(newPool);

        }
        /// <summary>
        /// Мутировать модель
        /// </summary>
        private void Mutate(ref SimModel sim)
        {
            SimModel simClone = sim.Clone();
            Random r = new Random(System.DateTime.Now.Millisecond);
            if ((ChangeMultiplicity) && (ChangeAtomicRes))
            {
                if (r.NextDouble() < 0.5)//c вероятностью 50%
                    MutateMultiplicity(ref sim);
                else
                    MutateResource(ref sim);
            }
            else
            {
                if (ChangeAtomicRes)
                    MutateResource(ref sim);
                if (ChangeMultiplicity)
                    MutateMultiplicity(ref sim);
            }
            //теперь проверим, удовлетворены ли критерии
            if (!AreConstraintsSatisfied(sim))
            {//если не удовлетворены, мутируем еще раз, с вероятностю 90%
                if (r.NextDouble() < 0.90)//не 100%, ибо может быть локальный тупик
                {
                    sim = simClone;//вернем, что было, и мутируем еще раз
                    Mutate(ref sim);
                }
                else
                    sim = simClone;//если не удовл. и выпал случай, вернем то, что было сначала
            }//если удовлетворены
            else
            {
                CalcTavr(ref sim);//посчитаем его среднее время
            }
        }

        private void MutateResource(ref SimModel sim)
        {
            Random r = new Random(System.DateTime.Now.Millisecond * 3);
            //получим случайный блок ресурсов
            int i = r.Next(sim.Resources.Count);
            Resource res = sim.Resources[i];
            //изменим 1 из его атомарных ресурсов на атомарный ресурс той же категории
            int index = r.Next(res.Atoms.Count);//получили номер атомарного ресурса
            AtomicRes ar = res.Atoms[index];

            AtomicRes ar2;//ресурс для замены
            //произвести замену можно, только если в категории больше 1 ресурса
            if (!Categories.ContainsKey(ar.Category))//если такой категории нет в списке из БД, ничего не делаем
                return;
            if(Categories[ar.Category].Count > 1){
                //пока не найдем нужный ресурс
                do
                {
                    //найдем случайный ресурс в той же категории
                    i = r.Next(Categories[ar.Category].Count);
                    ar2 = Categories[ar.Category][i];
                    //проверим, не является ли ar2 == ar
                }
                while (ar.Name == ar2.Name);
                //теперь заменим
                res.Atoms[index] = ar2;
            }

        }


        private void MutateMultiplicity(ref SimModel sim)
        {
            Random r = new Random(System.DateTime.Now.Millisecond*2);
            //получим случайный блок ресурсов
            int i = r.Next(sim.Resources.Count);
            Resource res = sim.Resources[i];
            //изменим его множественность на 1
            if (res.Shared)//если попали на разделяемый ресурс
            {
                if (r.NextDouble() < 0.9)//с вероятностью 90% попытаемся мутировать снова
                    MutateMultiplicity(ref sim);//не 100%, ибо в модели может не быть неразделяемых ресурсов
            }
            else
            {
                if (res.Multiplicity > 1)
                {
                    //если можно уменьшать
                    if (r.NextDouble() < 0.5)//c вероятностью 50%
                        res.Multiplicity++;
                    else
                        res.Multiplicity--;
                }
                else
                {
                    res.Multiplicity++;
                }
            }
        }

        private void InitiatePool(SimModel StartModel)
        {
            simPool.Clear();
            SimModel sim = StartModel.Clone();
            //создадим M начальных решений
            //0-е - исходное
            simPool.Add(sim);
            CalcTavr(ref sim);//calculate Tavr
            //остальные - мутанты
            for (int i = 1; i < M; i++)
            {
                //клонируем модель
                sim = StartModel.Clone();
                //мутируем ее и вычислим Tavr
                Mutate(ref sim);
                //добавим в набор
                simPool.Add(sim);
            }
        }

        /// <summary>
        /// Проверить, удовлетворены ли ограничения
        /// </summary>
        /// <param name="sim">Модель</param>
        /// <returns>ИСТИНА, если да, ЛОЖЬ, если нет</returns>
        private bool AreConstraintsSatisfied(SimModel sim)
        {
            bool b = true;
            double param = 0;
            for (int i = 0; i < ParamConstraints.Count; i++)
            {
                if (ParamConstraints[i] == 0)
                    continue;//нулевые ограничения не учитываются
                //проверим параметр i
                if (i == 0)
                    param = sim.Tavr;//нулевой параметр - это время вып. проекта
                else
                    param = sim.GetTotalParam(i - 1);//дополнительные (кроме времени) параметры
                if (ParamWeights[i] > 0)
                {
                    if (param > ParamConstraints[i])
                        b = false;
                }
                else
                {
                    if (ParamWeights[i] < 0)
                    {
                        if (param < ParamConstraints[i])
                            b = false;
                    }
                }
            }

            return b;
        }
        /// <summary>
        /// Приостановить оптимизацию
        /// </summary>
        public void Pause()
        {
            shouldOptimizationGo = false;
            Paused = true;
        }
        
        /// <summary>
        /// Остановить оптимизацию
        /// </summary>
        public void Stop()
        {
            shouldOptimizationGo = false;
            Paused = false;

        }

        //конструктор
        public SimOptimizer()
        {
            NRepeat = 1;
            ParamWeights = new List<double>();
            ParamConstraints = new List<double>();
            ChangeAtomicRes = false;
            ChangeMultiplicity = true;
        }

    }
}
